﻿#region license
# This file is part of Grawlix.
# 
# Grawlix is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of
# the License, or (at your option) any later version.
# 
# Grawlix is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public
# License along with Grawlix.  If not, see
# <http://www.gnu.org/licenses/>.
#endregion

"""Figure out comparison operators based on a comparison function."""

# Fixme: OK for value types, but there are caveats with unsealed class types.
# Derived types can add new members that affect all operations.

namespace Grawlix.Meta
import System
import Boo.Lang.Compiler

transient class ComparisonAttribute(AbstractAstAttribute):
"""Generate comparison statements based on one method."""
	
	override def Apply(targetNode as Ast.Node):
	"""
	Apply the attribute to the specified node.
	Param targetNode: The node to apply to.
	"""
		if Ast.NodeType.Method != targetNode.NodeType:
			InvalidNodeForAttribute('Method')
			return
		
		# Initialize data
		_method = targetNode as Ast.Method
		_methodReference = Ast.ReferenceExpression(_method.Name)
		_parentDefinition = _method.DeclaringType
		_parentTag = TypeSystem.TypeSystemServices. \
			GetEntity(_parentDefinition) as TypeSystem.IType
		_inverted = false
		
		if _method.Parameters.Count != 1 or _method.IsStatic:
			raise ArgumentException(
				"A ComparisonAttribute must be applied to a " +
				"non-static method with one parameter.")
		
		# Enter the parent namespace for name resolution
		# HACK: Doesn't like destructors, so I made it check
		# the module namespace instead of the parent namespace
		moduleTag = TypeSystem.TypeSystemServices. \
			GetEntity(enclosingModule(_method)) as TypeSystem.INamespace
		NameResolutionService.EnterNamespace(moduleTag)
		try:
			leftTag = _parentTag
			
			rightType = _method.Parameters[0].Type
			NameResolutionService.ResolveTypeReference(rightType)
			rightTag = TypeSystem.TypeSystemServices \
				.GetEntity(rightType) as TypeSystem.IType
			
			if not leftTag.IsFinal or not rightTag.IsFinal:
				# Assumptions
				assert leftTag.IsFinal or not leftTag.IsValueType
				assert rightTag.IsFinal or not rightTag.IsValueType
				raise ArgumentException(
					"In ComparisonAttributes, both the declaring type " +
					"and the type compared against must be value types " +
					"or sealed reference types. " +
					"There is no good way to represent comparisons " +
					"between reference types and values derived " +
					"from them. " + \
					"Consider using external comparers for specific " +
					"situtations where you need to sort references.")
			
			# Always call (.Equals always needs to be defined)
			CreateBaseMethods(rightTag)
			
			# Only call if the left type is the right type;
			# IEquatable[of T], IComparable and IComparable[of T]
			# should only compare the same types
			if leftTag == rightTag:
				ImplementInterface(rightTag)
			
			# Spawn operators
			if ShouldOverloadOperators():
				CreateOperators(leftTag, rightTag)
				
				# Spawn inverse operators
				_inverted = true
				if leftTag != rightTag:
					CreateOperators(rightTag, leftTag)
		ensure:
			NameResolutionService.LeaveNamespace()
		
	protected virtual def CreateBaseMethods([required] otherTag as \
	                                        TypeSystem.IType):
	"""
	Create base methods that always are needed.
	Param otherTag: The type entity to compare against.
		is a value type.
	"""
		pass
	
	private def InheritComparable([required] otherTag as TypeSystem.IType):
	"""
	Derive from IComparable.
	Param otherTag: The type entity to compare against.
	"""
		# Interface itself
		AddBase(Ast.SimpleTypeReference("System.IComparable"))
		
		otherType = CodeBuilder.CreateTypeReference(otherTag)
		call = CreateMethodCall(
			Ast.SelfLiteralExpression(),
			[| cast($otherType, $ObjIdentifier) |])
		exceptionText = \
			"Attempt to evaluate CompareTo on two objects " + \
			"of different type. " + \
			"CompareTo may only be used to compare objects of the " + \
			"same type. "
		
		objIdentifierName = ObjIdentifier.Name
		parentReference = CodeBuilder.CreateTypeofExpression(ParentTag)
		compare = [|
			def CompareTo($ObjIdentifier):
				if $ObjIdentifier is null:
					return 1
				if $ObjIdentifier.GetType() is not $parentReference:
					raise System.ArgumentException(
						$exceptionText,
						$objIdentifierName)
				return cast(int, $call)
		|]
		compare.Documentation = \
			"""
			Comparison function.
			Param ${ObjIdentifier.Name}: The object to compare against.
			Returns: Positive if greater than ${ObjIdentifier.Name} or
				${ObjIdentifier.Name} is null,
				negative if less than ${ObjIdentifier.Name},
				0 if equal.
			"""
		AddMember(compare)
	
	private def InheritGenericComparable([required] otherTag as \
	                                     TypeSystem.IType):
	"""
	Derive from IComparable[of T].
	Param otherTag: The type entity to compare against.
	"""
		otherType = CodeBuilder.CreateTypeReference(otherTag)
		baseGenericComparable = Ast.GenericTypeReference()
		baseGenericComparable.Name = "System.IComparable"
		baseGenericComparable.GenericArguments.Add(otherType)
		AddBase(baseGenericComparable)
		
		genCall = CreateMethodCall(Ast.SelfLiteralExpression(), OtherIdentifier)
		generic = [|
			def CompareTo($OtherIdentifier as $otherType):
				return cast(int, $genCall)
		|]
		generic.Documentation = \
			"""
			Comparison function.
			Param ${OtherIdentifier.Name}: The object to compare against.
			Returns: Positive if greater than ${OtherIdentifier.Name},
				negative if less than ${OtherIdentifier.Name},
				0 if equal.
			"""
		if not otherTag.IsValueType:
			PrependCheck(generic, OtherIdentifier, [| null |], [| 1 |])
		AddMember(generic)
	
	protected virtual def ImplementInterface([required] otherTag as \
	                                         TypeSystem.IType):
	"""
	Derive from interface and create the interface
	methods needed for comparison.
	Param otherTag: The type to compare against.
	"""
		InheritComparable(otherTag)
		InheritGenericComparable(otherTag)
	
	protected virtual def CreateOperators([required] leftTag as \
	                                      TypeSystem.IType,
	                                      [required] rightTag as \
	                                      TypeSystem.IType):
	"""
	Create the operators for the type.
	Param leftTag: The type entity of the left hand side of the operation.
	Param rightTag: The type entity of the right hand side of the operation.
	"""
		# Must produce all four separately to handle NaN correctly
		gt = SpawnOperator(
			Ast.BinaryOperatorType.GreaterThan,
			leftTag,
			rightTag)
		gte = SpawnOperator(
			Ast.BinaryOperatorType.GreaterThanOrEqual,
			leftTag,
			rightTag)
		lt = SpawnOperator(
			Ast.BinaryOperatorType.LessThan,
			leftTag,
			rightTag)
		lte = SpawnOperator(
			Ast.BinaryOperatorType.LessThanOrEqual,
			leftTag,
			rightTag)
		# Reverse order
		if not rightTag.IsValueType:
			PrependCheck(lt,  RhsIdentifier, [| null |], [| false |])
			PrependCheck(lte, RhsIdentifier, [| null |], [| false |])
			PrependCheck(gt,  RhsIdentifier, [| null |], [| true |])
			PrependCheck(gte, RhsIdentifier, [| null |], [| true |])
		if not leftTag.IsValueType:
			trueReturnValue as Ast.Expression
			falseReturnValue as Ast.Expression
			if rightTag.IsValueType:
				trueReturnValue = [| true |]
				falseReturnValue = [| false |]
			else:
				trueReturnValue = [| $RhsIdentifier is not null |]
				falseReturnValue = [| $RhsIdentifier is null |]
			PrependCheck(lt,  LhsIdentifier, [| null |], trueReturnValue)
			PrependCheck(lte, LhsIdentifier, [| null |], [| true |])
			PrependCheck(gt,  LhsIdentifier, [| null |], [| false |])
			PrependCheck(gte, LhsIdentifier, [| null |], falseReturnValue)
		AddMember(lt)
		AddMember(lte)
		AddMember(gt)
		AddMember(gte)
	
	private def SpawnOperator(operator as Ast.BinaryOperatorType,
	                          [required] leftTag as TypeSystem.IType,
	                          [required] rightTag as TypeSystem.IType):
	"""
	Create one operator.
	Param operator: The operator to use.
	Param leftTag: The left hand side of the operation.
	Param rightTag: The right hand side of the operation.
	"""
		if operator == Ast.BinaryOperatorType.GreaterThan:
			desc = "greater than"
		elif operator == Ast.BinaryOperatorType.GreaterThanOrEqual:
			desc = "greater than or equal to"
		elif operator == Ast.BinaryOperatorType.LessThan:
			desc = "less than"
		elif operator == Ast.BinaryOperatorType.LessThanOrEqual:
			desc = "less than or equal to"
		else:
			raise ArgumentException(
				"The operator specified must be a comparison operator.",
				"operator")
		capDesc = char.ToUpperInvariant(desc[0]) + desc[1:]
		operatorName = Ast.AstUtil.GetMethodNameForOperator(operator)
		operatorReference = Ast.ReferenceExpression(operatorName)
		leftType = CodeBuilder.CreateTypeReference(leftTag)
		rightType = CodeBuilder.CreateTypeReference(rightTag)
		returnValue = CodeBuilder. \
			CreateBoundBinaryExpression(
				TypeSystemServices.BoolType,
				operator,
				CreateMethodCall(LhsIdentifier, RhsIdentifier),
				[| 0 |])
		method = [|
			static def $operatorReference($LhsIdentifier as $leftType,
			                              $RhsIdentifier as $rightType):
				return $returnValue
		|]
		method.Documentation = \
			"""
			${capDesc} comparison.
			Param ${LhsIdentifier.Name}: The left hand side of the comparison.
			Param ${RhsIdentifier.Name}: The right hand side of the comparison.
			Returns: true if ${desc}, else false.
			"""
		return method
	
	protected virtual def InvertResult([required] result as Ast.Expression):
	"""
	Invert the result of the method call.
	Param result: The expression to invert.
	Returns: The inverted expression.
	"""
		return [| (-$result) |] as Ast.Expression
	
	protected virtual def ShouldOverloadOperators():
	"""
	Decide whether operators should be produced.
	Returns: true if yes, false if no.
	"""
		return true
	
	protected def AddBase([required] base as Ast.TypeReference):
	"""
	Add a base type to ParentType.
	Param base: The base to derive from.
	"""
		base.LexicalInfo = LexicalInfo
		_parentDefinition.BaseTypes.Add(base)
	
	protected def AddMember([required] member as Ast.TypeMember):
	"""
	Add a type member to ParentType.
	Param member: The member to add.
	"""
		member.LexicalInfo = LexicalInfo
		_parentDefinition.Members.Add(member)
	
	protected def CreateMethodCall([required] lhs as Ast.Expression,
	                               [required] rhs as Ast.Expression):
	"""
	Create a method call expression.
	Param lhs: The left hand side of the method call.
	Param rhs: The right hand side of the method call.
	Returns: A method call expression.
	"""
		if not _inverted:
			return [| $lhs.$_methodReference($rhs) |]
		else:
			return InvertResult([| $rhs.$_methodReference($lhs) |])
	
	static protected def PrependCheck(
		[required] target as Ast.Method,
		[required] lhs as Ast.Expression,
		[required] rhs as Ast.Expression,
		[required] failureResult as Ast.Expression):
	"""
	Add check on a given method to return failure if null.
	Param target: The method to add checks to.
	Param lhs: The left-hand expression to check against.
	Param lhs: The right-hand expression to check against.
	Param failureResult: What to return upon fail.
	"""
		body = target.Body
		target.Body = [|
			return $failureResult if $lhs is $rhs
			$body
		|]
	
	[getter(Method, Protected: true)]
	private _method as Ast.Method
	"""The original method."""
	
	[getter(ParentDefinition, Protected: true)]
	private _parentDefinition as Ast.TypeDefinition
	"""The type the method is defined in."""
	
	[getter(ParentTag, Protected: true)]
	private _parentTag as TypeSystem.IType
	"""Whether the parent type is a value type."""
	
	private _methodReference as Ast.Expression
	"""A reference to the original method."""
	
	private _inverted as bool
	"""Whether the call is inverted."""
	
	[getter(ObjIdentifier, Protected: true)]
	private static _objIdentifier = Ast.ReferenceExpression("obj")
	"""An identifier named 'obj'."""
	
	[getter(OtherIdentifier, Protected: true)]
	private static _otherIdentifier = Ast.ReferenceExpression("other")
	"""An identifier named 'other'."""
	
	[getter(LhsIdentifier, Protected: true)]
	private static _lhsIdentifier = Ast.ReferenceExpression("lhs")
	"""An identifier referring to the left hand side of a comparison."""
	
	[getter(RhsIdentifier, Protected: true)]
	private static _rhsIdentifier = Ast.ReferenceExpression("rhs")
	"""An identifier referring to the right hand side of a comparison."""
	
